﻿using System;
using System.Collections.Generic;
using DatabaseGroup.SSIS.SourceAdapters.Parser;
using Microsoft.SqlServer.Dts.Pipeline;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;
using System.Text.RegularExpressions;

namespace DatabaseGroup.SSIS.SourceAdapters
{
    /// <summary>
    /// Handles the processing of output rows for the component buffers,
    /// including redirecting the row to error/log outputs when required
    /// </summary>
    internal class BufferSink
    {
        private readonly IComponentBufferService _bufferService;

        /// <summary>
        /// The standard output
        /// </summary>
        private readonly IDTSOutput100 _output;

        private Int64 _currentRowCount = 0;
        private int _configuredStandardOutputColumns = 0;
        private bool _treatEmptyStringAsNull = true;
        private bool _includeErrorAuditRows;
        private bool _trimWhiteSpace;
        private List<int> _standardOutputColumns;
        // Matches any start/end white space
        private Regex _cleanWhiteSpace = new Regex(@"^[\s\n\r]*|[\s\n\r]*$", RegexOptions.Compiled);
        // Matches newlines and any surrounding white space and accounts for commas
        private Regex _flattenLines = new Regex(@",\s*[\n\r]+\s*|\s*[\n\r]+\s*,|\s*[\n\r]+\s*", RegexOptions.Compiled);

        /// <summary>
        /// Initializes a new instance of the BufferSink class 
        /// associated with an output mapped to an underlying component buffer service
        /// </summary>
        /// <param name="bufferService">The bufferservice to use for data operations</param>
        /// <param name="output">The SSIS output to use</param>
        /// <param name="treatEmptyStringAsNull">Should empty strings be treated as null values</param>
        /// <param name="includeErrorAuditRows">When a row is send to the error output should column error audit rows be included</param>
        public BufferSink(IComponentBufferService bufferService, IDTSOutput100 output, bool treatEmptyStringAsNull, 
            bool includeErrorAuditRows, bool trimWhiteSpace)
        {
            ArgumentVerifier.CheckObjectArgument(bufferService, "bufferService");
            ArgumentVerifier.CheckObjectArgument(output, "output");

            _bufferService = bufferService;
            _output = output;
            _treatEmptyStringAsNull = treatEmptyStringAsNull;
            _includeErrorAuditRows = includeErrorAuditRows;
            _trimWhiteSpace = trimWhiteSpace;
        }

        public string RowDelimiter { get; set; }

        /// <summary>
        /// Gets or sets the current row count
        /// </summary>
        public Int64 CurrentRowCount
        {
            get { return _currentRowCount; }
            set { _currentRowCount = value; }
        }

        /// <summary>
        /// Configures the BufferSink with a list of file columns
        /// that have been marked for use as component output columns
        /// </summary>
        /// <param name="columns">The list of configured file columns</param>
        public void DefineStandardOutputColumns(List<ConfiguredFileColumn> columns)
        {
            _configuredStandardOutputColumns = columns.Count;
            _standardOutputColumns = new List<int>();

            for (int colIndex = 0; colIndex < columns.Count; colIndex++)
            {
                if (columns[colIndex].IsOutput)
                    _standardOutputColumns.Add(colIndex);
            }
        }

        /// <summary>
        /// Takes a single row of data and processes it to the output
        /// </summary>
        /// <param name="rowData">The row data to handle</param>
        public void AddRow(RowData rowData)
        {
            _currentRowCount++;
            if (!CheckRowOverflow(rowData))
            {
                _bufferService.AddRow();
                AddColumns(rowData);
            }
        }

        /// <summary>
        /// A simple lookup to convert an int error code to a
        /// user friendly message
        /// </summary>
        /// <param name="errorCode">The error code to lookup</param>
        /// <returns>The user friendly error message string</returns>
        public static string MapErrorCodeToMessage(int errorCode)
        {
            switch (errorCode)
            {
                case (int)ErrorCode.ColumnLevelErrorTruncationOperation:
                    return MessageStrings.ColumnLevelErrorTruncationOperation;
                case (int)ErrorCode.ColumnLevelErrorFormatOperation:
                    return MessageStrings.ColumnLevelErrorFormatOperation;
                case (int)ErrorCode.ColumnLevelErrorParseOperation:
                    return MessageStrings.ColumnLevelErrorParseOperation;
                case (int)ErrorCode.RowLevelTruncationOperation:
                    return MessageStrings.RowLevelTruncationOperation;
                case (int)ErrorCode.RowLevelUnderflowOperation:
                    return MessageStrings.RowLevelUnderflowOperation;
                default:
                    return "Unknown processing error occurred.";
            }
        }

        /// <summary>
        /// Checks to see if the number of columns within the row is more than expected
        /// </summary>
        /// <param name="rowData">The row data to check</param>
        /// <returns>True if the row was handled, false if the row still needs to be handled by the main output</returns>
        private bool CheckRowOverflow(RowData rowData)
        {
            if (rowData.ColumnCount > _configuredStandardOutputColumns)
            {
                string errorMessage = MessageStrings.RowOverflow(_currentRowCount, rowData.ColumnCount, _bufferService.ColumnCount);

                if (_bufferService.ErrorOutputUsed && _output.TruncationRowDisposition == DTSRowDisposition.RD_RedirectRow)
                {
                    _bufferService.AddErrorRow((int)ErrorCode.RowLevelTruncationOperation, errorMessage, _currentRowCount, CleanRowDelimiter(rowData.RowText), true, null, null);
                    return true;
                }

                if (_bufferService.LogOutputUsed && _output.TruncationRowDisposition == DTSRowDisposition.RD_IgnoreFailure)
                {
                    _bufferService.AddLogRow((int)ErrorCode.RowLevelTruncationOperation, errorMessage, _currentRowCount, null, null);
                    return false;
                }

                if (_output.TruncationRowDisposition == DTSRowDisposition.RD_FailComponent)
                    throw new BufferSinkException(errorMessage);
            }

            return false;
        }

        /// <summary>
        /// Adds all the column data held within the row to the buffer output
        /// </summary>
        /// <param name="rowData">The row data containing the columns to add</param>
        private void AddColumns(RowData rowData)
        {
            bool isRowError = false;
            List<ColumnError> rowErrors = null;

            for (int outputColIndex = 0; outputColIndex < _bufferService.ColumnCount; outputColIndex++)
            {
                if (outputColIndex < rowData.ColumnCount)
                {
                    string columnData;

                    try
                    {
                        columnData = rowData.GetColumnData(_standardOutputColumns[outputColIndex]);
                    }
                    catch (Exception ex)
                    {
                        throw new BufferSinkException(MessageStrings.FailedToFindFileColumnIndex, ex);
                    }

                    if (_trimWhiteSpace && !string.IsNullOrEmpty(columnData))
                    {
                        columnData = _cleanWhiteSpace.Replace(columnData, "");
                        columnData = _flattenLines.Replace(columnData, ", ");
                    }

                    if (_treatEmptyStringAsNull && string.IsNullOrEmpty(columnData))
                    {
                        _bufferService.SetNull(outputColIndex);
                    }
                    else
                    {
                        try
                        {
                            _bufferService.SetColumnData(outputColIndex, columnData);
                        }
                        catch (Exception ex)
                        {
                            IDTSOutputColumn100 outputColumn = _output.OutputColumnCollection[outputColIndex];
                            if (ex is DoesNotFitBufferException || ex is OverflowException || ex is System.Data.SqlTypes.SqlTruncateException)
                            {
                                // if configured to log/ignore error and the log output is not being used - just ignore and continue
                                if (outputColumn.TruncationRowDisposition == DTSRowDisposition.RD_IgnoreFailure && !_bufferService.LogOutputUsed)
                                    continue;
                            }

                            if (!isRowError)
                            {
                                isRowError = true;
                                rowErrors = new List<ColumnError>();
                            }

                            rowErrors.Add(new ColumnError(outputColumn, columnData, MapExceptionToErrorCode(ex), ex));

                            // if we're not auditing all errors on the row, simply handle the first one
                            if (!_includeErrorAuditRows)
                                break;
                        }
                    }
                }
                else
                {
                    if (_output.TruncationRowDisposition == DTSRowDisposition.RD_RedirectRow ||
                        (_bufferService.LogOutputUsed && _output.TruncationRowDisposition == DTSRowDisposition.RD_IgnoreFailure))
                    {
                        // TODO: get rid of code duplication (copied from above)
                        if (!isRowError)
                        {
                            isRowError = true;
                            rowErrors = new List<ColumnError>();
                        }

                        IDTSOutputColumn100 outputColumn = _output.OutputColumnCollection[outputColIndex];
                        Exception ex = new IndexOutOfRangeException(MessageStrings.RowLevelUnderflowOperation);
                        rowErrors.Add(new ColumnError(outputColumn, "", MapExceptionToErrorCode(ex), ex));

                        // because this is a row level error, force the column processing to stop
                        for (; outputColIndex < _bufferService.ColumnCount; outputColIndex++)
                        {
                            _bufferService.SetNull(outputColIndex);
                        }

                        break;
                    }

                    if (_output.TruncationRowDisposition == DTSRowDisposition.RD_FailComponent)
                        throw new BufferSinkException(MessageStrings.RowOverflow(_currentRowCount, rowData.ColumnCount, _bufferService.ColumnCount));

                    _bufferService.SetNull(outputColIndex);
                }
            }

            if (isRowError)
                HandleRowErrors(rowData.RowText, _currentRowCount, rowErrors);
        }

        /// <summary>
        /// Maps specific exceptions to an int error code
        /// </summary>
        /// <param name="ex">The exception to map</param>
        /// <returns>The error code int</returns>
        private int MapExceptionToErrorCode(Exception ex)
        {
            if (ex is DoesNotFitBufferException || ex is OverflowException || ex is System.Data.SqlTypes.SqlTruncateException)
                return (int)ErrorCode.ColumnLevelErrorTruncationOperation;
            if (ex is FormatException)
                return (int)ErrorCode.ColumnLevelErrorFormatOperation;
            if (ex is IndexOutOfRangeException)
                return (int)ErrorCode.RowLevelUnderflowOperation;
            // TODO: MessageStrings
            if (ex is BufferSinkException && ex.Message.Contains("greater than the number"))
                return (int)ErrorCode.RowLevelTruncationOperation;
            return (int)ErrorCode.UnknownErrorParseOperation;
        }

        // TODO: Really hate the name of this method - you'd surely think ALL errors should be invalid
        /// <summary>
        /// Determines based on the disposition configuration if an error
        /// is valid (allowable) or invalid (don't allow in standard output)
        /// </summary>
        /// <param name="errorCode">The error which has occurred</param>
        /// <param name="truncationDisposition">The truncation disposition setting</param>
        /// <param name="errorDisposition">The error disposition setting</param>
        /// <returns>True if error is invalid, false if valid</returns>
        private bool IsErrorInvalid(int errorCode, DTSRowDisposition truncationDisposition, DTSRowDisposition errorDisposition)
        {
            // if there was an error condition, check to see how to handle based on the errorDisposition
            if (errorCode >= (int)ErrorCode.UnknownErrorParseOperation &&
                (errorDisposition == DTSRowDisposition.RD_RedirectRow || errorDisposition == DTSRowDisposition.RD_FailComponent))
            {
                return true;
            }

            // if there was a truncation, check to see how to handle based on the truncationDisposition
            if (errorCode == (int)ErrorCode.ColumnLevelErrorTruncationOperation && 
                (truncationDisposition == DTSRowDisposition.RD_RedirectRow || truncationDisposition == DTSRowDisposition.RD_FailComponent))
            {
                return true;
            }

            // column format errors
            if (errorCode == (int)ErrorCode.ColumnLevelErrorFormatOperation &&
                (errorDisposition == DTSRowDisposition.RD_RedirectRow || errorDisposition == DTSRowDisposition.RD_FailComponent))
            {
                return true;
            }

            // if there was a row level truncation, check to see how to handle based on the output truncationDisposition
            if ((errorCode == (int)ErrorCode.RowLevelTruncationOperation ||
                errorCode == (int)ErrorCode.RowLevelUnderflowOperation) &&
                (_output.TruncationRowDisposition == DTSRowDisposition.RD_RedirectRow ||
                _output.TruncationRowDisposition == DTSRowDisposition.RD_FailComponent))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Loops through any row errors and handles them based on the 
        /// disposition configuration
        /// </summary>
        /// <param name="rowData">The row data being handled</param>
        /// <param name="lineNumber">The line number of the source file</param>
        /// <param name="rowErrors">A list of row errors</param>
        private void HandleRowErrors(string rowData, long lineNumber, IEnumerable<ColumnError> rowErrors)
        {
            ColumnError fatalColumnError = null;
            bool doSilentDrop = true;

            foreach (ColumnError colError in  rowErrors)
            {
                // if any one column results in an error redirect, then redirect all row errors to the error output
                if (IsErrorInvalid(colError.ErrorCode, colError.TruncationRowDisposition, colError.ErrorRowDisposition))
                {
                    fatalColumnError = colError;
                    break;
                }

                if (colError.ErrorCode == (int)ErrorCode.ColumnLevelErrorFormatOperation
                    && colError.ErrorRowDisposition == DTSRowDisposition.RD_IgnoreFailure
                    && _bufferService.LogOutputUsed)
                {
                    doSilentDrop = false;
                }

                if (colError.ErrorCode == (int)ErrorCode.ColumnLevelErrorTruncationOperation
                    && colError.TruncationRowDisposition == DTSRowDisposition.RD_IgnoreFailure 
                    && _bufferService.LogOutputUsed)
                {
                    doSilentDrop = false;
                }

                if ((colError.ErrorCode == (int)ErrorCode.RowLevelTruncationOperation ||
                    colError.ErrorCode == (int)ErrorCode.RowLevelUnderflowOperation)
                    && _output.TruncationRowDisposition == DTSRowDisposition.RD_IgnoreFailure
                    && _bufferService.LogOutputUsed)
                {
                    doSilentDrop = false;
                }
            }

            if (fatalColumnError != null)
                HandleRowErrorRedirect(rowData, lineNumber, rowErrors, fatalColumnError);
            else if (!doSilentDrop)
                HandleRowLog(lineNumber, rowErrors);
            else
                // nothing to send to the log or error outputs, so silently drop the row
                _bufferService.RemoveRow();
        }

        /// <summary>
        /// Adds an error row for each of the errors found in processing a row of data
        /// </summary>
        /// <param name="rowData">The row data that triggered the error(s)</param>
        /// <param name="lineNumber">The line number of the source file</param>
        /// <param name="rowErrors">A list of errors that occurred in processing the row</param>
        /// <param name="fatalError">The first error that triggered the error to be written</param>
        private void HandleRowErrorRedirect(string rowData, long lineNumber, IEnumerable<ColumnError> rowErrors, ColumnError fatalError)
        {
            string errorMessage = MapErrorCodeToMessage(fatalError.ErrorCode);

            if (fatalError.ErrorCode == (int)ErrorCode.UnknownErrorParseOperation)
                errorMessage = CommonUtils.LimitStringLength(fatalError.OriginalException.Message, 255);

            _bufferService.AddErrorRow(fatalError.ErrorCode, errorMessage, lineNumber, CleanRowDelimiter(rowData), 
                true, fatalError.Name, fatalError.Data);

            foreach (ColumnError colError in rowErrors)
            {
                if (colError == fatalError)
                    continue;

                errorMessage = MapErrorCodeToMessage(colError.ErrorCode);

                if (colError.ErrorCode == (int)ErrorCode.UnknownErrorParseOperation)
                    errorMessage = CommonUtils.LimitStringLength(colError.OriginalException.Message, 255);

                _bufferService.AddErrorRow(colError.ErrorCode, errorMessage, lineNumber, CleanRowDelimiter(rowData),
                    IsErrorInvalid(colError.ErrorCode, colError.TruncationRowDisposition, colError.ErrorRowDisposition), colError.Name, colError.Data);
            }

            _bufferService.RemoveRow();

            if (!(fatalError.OriginalException is DoesNotFitBufferException) 
                && !(fatalError.OriginalException is OverflowException) 
                && !(fatalError.OriginalException is System.Data.SqlTypes.SqlTruncateException)
                && !(fatalError.OriginalException is FormatException)
                && !(fatalError.OriginalException is IndexOutOfRangeException)
                && !(fatalError.OriginalException is BufferSinkException))
            {
                throw new BufferSinkException(MapErrorCodeToMessage(fatalError.ErrorCode), fatalError.OriginalException);
            }
        }

        /// <summary>
        /// Adds a log row for each of the row errors found in processing a row of data
        /// </summary>
        /// <param name="lineNumber">The line number of the source file</param>
        /// <param name="rowErrors">A list of errors that occurred in processing the row</param>
        private void HandleRowLog(long lineNumber, IEnumerable<ColumnError> rowErrors)
        {
            foreach (ColumnError colError in rowErrors)
                _bufferService.AddLogRow(colError.ErrorCode, MapErrorCodeToMessage(colError.ErrorCode), lineNumber, colError.Name, colError.Data);
        }

        /// <summary>
        /// Removes the row delimiter from the row data
        /// </summary>
        /// <param name="data">The row data to be cleaned</param>
        /// <returns>The clean row data</returns>
        private string CleanRowDelimiter(string data)
        {
            if (!string.IsNullOrEmpty(this.RowDelimiter) && data.EndsWith(this.RowDelimiter))
                data = data.Remove(data.Length - this.RowDelimiter.Length);

            return data;
        }
    }

    #region Internal classes

    /// <summary>
    /// Stores information about a column settings and the associated 
    /// error information
    /// </summary>
    internal class ColumnError
    {
        /// <summary>
        /// Gets the TruncationRowDisposition setting of the column the error relates to
        /// </summary>
        internal DTSRowDisposition TruncationRowDisposition { get; private set; }

        /// <summary>
        /// Gets the ErrorRowDisposition setting of the column the error relates to
        /// </summary>
        internal DTSRowDisposition ErrorRowDisposition { get; private set; }

        /// <summary>
        /// Gets the Name of the column the error relates to
        /// </summary>
        internal string Name { get; private set; }

        /// <summary>
        /// Gets the DataType of the column the error relates to
        /// </summary>
        internal DataType DataType { get; private set; }

        /// <summary>
        /// Gets the Length setting of the column the error relates to
        /// </summary>
        internal int Length { get; private set; }

        /// <summary>
        /// Gets the data that caused the column error
        /// </summary>
        internal string Data { get; private set; }

        /// <summary>
        /// Gets the error code identifying the error
        /// </summary>
        internal int ErrorCode { get; private set; }

        /// <summary>
        /// Gets the orginal exception that caused the column error
        /// </summary>
        internal Exception OriginalException { get; private set; }

        /// <summary>
        /// Initializes a new instance of the ColumnError class 
        /// using the column information provided
        /// </summary>
        /// <param name="outputColumn">The output column the error relates to</param>
        /// <param name="columnData">The column data that caused the error</param>
        /// <param name="errorCode">The error code</param>
        /// <param name="ex">The original exception that caused the column error</param>
        internal ColumnError(IDTSOutputColumn100 outputColumn, string columnData, int errorCode, Exception ex)
        {
            this.TruncationRowDisposition = outputColumn.TruncationRowDisposition;
            this.ErrorRowDisposition = outputColumn.ErrorRowDisposition;
            this.Name = outputColumn.Name;
            this.DataType = outputColumn.DataType;
            this.Length = outputColumn.Length;
            this.Data = columnData;
            this.ErrorCode = errorCode;
            this.OriginalException = ex;
        }
    }

    /// <summary>
    /// Simple decorator to indicate a buffer sink specific exception
    /// </summary>
    internal class BufferSinkException : Exception
    {
        /// <summary>
        /// Initializes a new instance of the BufferSinkException
        /// class using the exception message
        /// </summary>
        /// <param name="message">A message describing the exception</param>
        public BufferSinkException(string message)
            : base(message)
        {
        }

        /// <summary>
        /// Initializes a new instance of the BufferSinkException
        /// class using the exception message and orginal exception
        /// </summary>
        /// <param name="message">A message describing the exception</param>
        /// <param name="innerException">The original exception to cause the buffer sink error</param>
        public BufferSinkException(string message, Exception innerException)
            : base(message, innerException)
        {
        }
    }

    #endregion
}
